#ifndef SMTEST_HPP
#define SMTEST_HPP

#include <QtOpenGL>
#include <QGLShader>
#include <QGLBuffer>
#include <QMatrix>
#include <QQuaternion>
#include <QGLFramebufferObject>
#include <iostream>
#include <cmath>
#include <cfloat>
#include "camera.hpp"
#include "Light.hpp"
#include "loader.hpp"
#include "PolyList.hpp"
#include "CubeBox.hpp"

#define EPSILON 0.000001
#define CROSS(dest, v1, v2) \
    dest[0] = v1[1] * v2[2] - v1[2] * v2[1]; \
    dest[1] = v1[2] * v2[0] - v1[0] * v2[2]; \
    dest[2] = v1[0] * v2[1] - v1[1] * v2[0];

#define DOT(v1,v2) (v1[0]*v2[0]+v1[1]*v2[1]+v1[2]*v2[2])

#define SUB(dest,v1,v2) \
    dest[0] = v1[0] - v2[0]; \
    dest[1] = v1[1] - v2[1]; \
    dest[2] = v1[2] - v2[2];

using namespace std;

static inline QGLShaderProgram *createShader(char * vertexShaderFile, char * fragmentShaderFile)
{
    QGLShaderProgram *programShader = new QGLShaderProgram();

    QGLShader *vertexShader = NULL, *fragmentShader = NULL;
    if(vertexShaderFile != NULL)
    {
        vertexShader = new QGLShader(QGLShader::Vertex);
		if(vertexShader->compileSourceFile(vertexShaderFile))
            programShader->addShader(vertexShader);
        else
            qWarning() << vertexShaderFile << vertexShader->log();
    }
    if(fragmentShaderFile != NULL)
    {
        fragmentShader = new QGLShader(QGLShader::Fragment);
		if(fragmentShader->compileSourceFile(fragmentShaderFile))
            programShader->addShader(fragmentShader);
        else
            qWarning() << fragmentShaderFile << fragmentShader->log();
    }

    if(!programShader->link())
        qWarning() << "Linker Error: " << vertexShaderFile  << " "<< fragmentShaderFile << programShader->log();

    if(vertexShader != NULL)
        delete(vertexShader);
    if(fragmentShader != NULL)
        delete(fragmentShader);
    return programShader;
}

template<typename T> static inline QGLBuffer createVBO(QGLBuffer::Type bufferType, QGLBuffer::UsagePattern usagePattern, std::vector<T> &data)
{
    QGLBuffer self(bufferType);
    self.create();
    self.bind();
    self.setUsagePattern(usagePattern);
    self.allocate(data.data(), data.size()*sizeof(T));
    return self;
};

static inline std::vector<QVector3D> jittering(QVector3D vertex1, QVector3D vertex2, QVector3D vertex3, int numberDivision)
{
    std::vector<QVector3D> self;
    QVector3D vector1 = (vertex2 - vertex1)/numberDivision;
    QVector3D vector2 = (vertex3 - vertex1)/numberDivision;

    srand(time(NULL));
    for(int i = 0; i < numberDivision; i++)
    {
        for(int j = 0; j < numberDivision; j++)
        {
            float rand1 = rand()%101 / 100.0;
            float rand2 = rand()%101 / 100.0;
            self.push_back(vertex1 + i*vector1 + rand1*vector1 + j*vector2 + rand2*vector2);
        }
    }
    return self;
}

static inline std::vector<QVector3D> jitteringLight(Light light, float size, int numberDivision)
{
    QVector3D center = light.getEye();
    QVector3D up = light.getUp();
    QVector3D target = light.getTarget();
    QVector3D v1 = QVector3D::crossProduct(up, target);
    QVector3D v2 = QVector3D::crossProduct(v1, target);

    QVector3D vertex1 = center - (v1*size)/2.0 - (v2*size)/2.0;
    QVector3D vertex2 = vertex1 + v1*size;
    QVector3D vertex3 = vertex1 + v2*size;
    return jittering(vertex1, vertex2, vertex3, numberDivision);
}

/* Determine if a ray intersect or not a triangle.
   Extracted from Moller, T., Trumbore, B., "Fast, Minimum Storage Ray / Triangle Intersection", Journal of Graphics Tools, 2(1):21--28, 1997. */
static bool intersectTriangle(double orig[3], double dir[3], double vert0[3], double vert1[3], double vert2[3], double & t, double & u, double & v)
{
    double edge1[3], edge2[3], tvec[3], pvec[3], qvec[3];
    double det, inv_det;

    /* find vectors for two egdes sharing vert0 */
    SUB(edge1, vert1, vert0);
    SUB(edge2, vert2, vert0);

    /* begin calculating determinant - also used to calculate U parameter */
    CROSS(pvec, dir, edge2);

    /* if determinant is near zero, ray lies in plane of triangle */
    det = DOT(edge1, pvec);


    if( det > - EPSILON && det < EPSILON )
        return false;
    inv_det = 1.0 / det;

    /* calculate distance from vert0 to ray origin */
    SUB(tvec, orig, vert0);

    /* calculate U parameter and test bounds */
    u = DOT(tvec, pvec) * inv_det;
    if( u < 0.0 || u > 1.0 )
        return false;

    /* prepare to test V parameter */
    CROSS(qvec, tvec, edge1);

    /* calculate V parameters and test bounds */
    v = DOT(dir, qvec) * inv_det;
    if( v < 0.0 || u + v > 1.0 )
        return false;

    /* calculate t, rays intersects triangle */
    t = DOT(edge2, qvec) * inv_det;

    return true;
}

/*
    Generate a random ray pointing into the same half-space as of the target vector.
*/
static inline QVector3D generateRay(QVector3D target)
{
    QVector3D dir;
    dir.setX(((double)rand() / (double)RAND_MAX) * 2.0 - 1.0);
    dir.setY(((double)rand() / (double)RAND_MAX) * 2.0 - 1.0);
    dir.setZ(((double)rand() / (double)RAND_MAX) * 2.0 - 1.0);
    if(QVector3D::dotProduct(dir, target) <=0)
        dir = - dir;
    dir.normalize();
    return dir;
}

static inline Light generateVPL(Light light, PolyList polyList)
{
    //srand(time(NULL));
    QVector3D ray = generateRay(light.getTarget());
    Light vpl(light.getIntensity()/2.0);
    double orig[3] = {light.getEye().x(), light.getEye().y(), light.getEye().z()} ;
    double dir[3] = {ray.x(), ray.y(), ray.z()};
    double vert0[3], vert1[3], vert2[3];
    double t = 0, u = 0, v = 0, dist = FLT_MAX;
    for(int i = 0; i < polyList.getListSize(); ++i)
    {
        vert0[0] = polyList.getPoly(i).getA().x();
        vert0[1] = polyList.getPoly(i).getA().y();
        vert0[2] = polyList.getPoly(i).getA().z();
        vert1[0] = polyList.getPoly(i).getB().x();
        vert1[1] = polyList.getPoly(i).getB().y();
        vert1[2] = polyList.getPoly(i).getB().z();
        vert2[0] = polyList.getPoly(i).getC().x();
        vert2[1] = polyList.getPoly(i).getC().y();
        vert2[2] = polyList.getPoly(i).getC().z();
        if(intersectTriangle(orig, dir, vert0, vert1, vert2, t, u, v) && t < dist)
        {
            dist = t;
            vpl.setEye(Polygon3P::baryToCart(u, v, polyList.getPoly(i)));
            QVector3D vpl_target = QVector3D::crossProduct(polyList.getPoly(i).getB() - polyList.getPoly(i).getA(), polyList.getPoly(i).getC() - polyList.getPoly(i).getA());
            if(QVector3D::dotProduct(vpl_target, ray) < 0)
                vpl_target = - vpl_target;
            vpl.setTarget(vpl_target);
        }
    }
    return vpl;
}


class ShaderExample
{
public:
    ShaderExample();
    void draw();
    void init();

public slots:
	void setShadowSize(int width, int height)
	{
        shadowSize.setWidth(width);
        shadowSize.setHeight(height);
    }
	void resize(int width, int height)
	{
        viewSize.setWidth(width);
        viewSize.setHeight(height);
        view.setPerspective(view.getFovy(), (float)width/height, view.getzNear(), view.getzFar());
    }
	void animateLight()
	{
        mainLight.rotate(0.0, 1.0, 0.0, 2.0);
        for(size_t i = 0; i < subLights.size(); i++)
            subLights.at(i).rotate(0.0, 1.0, 0.0, 2.0);
    }

    void addModel(const QString &filePath)
    {
        Model3D model;
        loadModel(filePath.toStdString(), model);
        modelList.clear();
        modelList.push_back(model);
    }

	void turnView(float x, float y)
	{
        view.turnYourself(x, y);
    }
	void translateView(float x, float y, float z)
	{
        view.translate(x, y, z);
    }

protected:
    void initShader();
    void initFramebuffer();
    void initMatrices();
    void drawShadowMaps();
    void drawSceneWithShadow();
    void drawScene();
    void drawSceneSample();
    void drawLights();
    void drawVPLs();
    void drawMasterTexture();

private:
    QGLShaderProgram *shadowRenderProgramShader, *shadowProgramShader, *convolutionProgramShader, *sceneRenderProgramShader;

    GLuint masterFramebuffer;
    GLuint masterTexture;
    GLuint shadowFramebuffer;
    GLuint shadowTexture;

    QSize shadowSize;
    QSize viewSize;

    Camera view;
    Light mainLight;
    std::vector<Light> subLights;

    QList<Model3D> modelList;

	CubeBox cubebox;
	std::vector<Light> vplList;
};

#endif // SMTEST_HPP
